// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Actors;
using System.Threading.Actors.Utilities;
using System.Threading.Tasks;

namespace TestActorMethods
{
    [TestClass]
    public class JsonSerializationUnitTests
    {

        [TestMethod]
        public void ActorServiceRequestSerTest()
        {
            var asreq = new ActorServiceRequest("MyMethod", "MyId", 33, "someStringParameter", 42);
            var json = asreq.ToJson();
            var asreq2 = ActorServiceRequest.FromJson(json);

            VerifyRequestsSame(asreq, asreq2);

            asreq = new ActorServiceRequest("Foo", "MyId", 42, new string[] { "uno", "dos" }, new int[] { 1, 2 });
            json = asreq.ToJson();
            asreq2 = ActorServiceRequest.FromJson(json);

            VerifyRequestsSame(asreq, asreq2);
        }

        public ActorEvent CreateEvent(string eventType, object payload, params object[] qualifiers)
        {
            return new ActorEvent(eventType, payload, qualifiers);
        }

        [TestMethod]
        public void ActorEventSerTest()
        {
            var ae1 = new ActorEvent("EventType1", Tuple.Create(42, "Forty-Two"), "Five", 5);
            var json = ae1.ToJson();
            var ae2 = ActorEvent.FromJson(json);

            VerifyEventsSame<Tuple<int, string>>(ae1, ae2);

            ae1 = new ActorEvent("EventType2", 42, new string[] {"Uno", "Dos"}, new int[] {1,2});
            json = ae1.ToJson();
            ae2 = ActorEvent.FromJson(json);

            VerifyEventsSame<int>(ae1, ae2);

            ae1 = new ActorEvent("EventType3", 13);
            json = ae1.ToJson();
            ae2 = ActorEvent.FromJson(json);

            VerifyEventsSame<int>(ae1, ae2);

            // Test that "params object[]" for qualifiers doesn't do anything weird
            ae1 = CreateEvent("EventType4", 55);
            json = ae1.ToJson();
            ae2 = ActorEvent.FromJson(json);

            VerifyEventsSame<int>(ae1, ae2);


        }

        [TestMethod]
        public void ActorServiceResultSerTest()
        {
            var res1 = ActorServiceResult.FromResult(34, Tuple.Create(43, "Forty-three"));
            var json = res1.ToJson();
            var res2 = ActorServiceResult.FromJson(json);

            VerifyResultsSame<Tuple<int, string>>(res1, res2);

            res1 = ActorServiceResult.FromResult(433, 17);
            json = res1.ToJson();
            res2 = ActorServiceResult.FromJson(json);

            VerifyResultsSame<int>(res1, res2);

            var e = new InvalidOperationException("Some Message");
            // Let's set the exception's stack trace too.
            try
            {
                throw e;
            }
            catch (InvalidOperationException ex)
            {
                e = ex;
            }
            res1 = ActorServiceResult.FromException(465, e);
            json = res1.ToJson();
            res2 = ActorServiceResult.FromJson(json);

            VerifyResultsSame<object>(res1, res2);
        }

        [TestMethod]
        public void BasicSerializationTest()
        {
            // JSON round-tripping with numbers gets really sloppy, because Javascript's Number type
            // is a bit messy.  So we've got some peculiar logic to handle that here.
            Console.WriteLine("Basic serialization test");

            Console.WriteLine("Testing numbers");
            int five = 5;
            int deserialized = SerializationRoundTrip(five);
            Console.WriteLine("Serialized {0}, and deserialized {1}", five, deserialized);
            Assert.AreEqual(five, deserialized);

            long largeNum = 1L << 53; // Javascript Number type's max integer value
            long deserializedLong = SerializationRoundTrip(largeNum);
            Console.WriteLine("Serialized {0}, and deserialized {1}", largeNum, deserializedLong);
            Assert.AreEqual(largeNum, deserializedLong);

            Console.WriteLine("Testing an Object[] containing some ints and other types");
            Object[] array = new Object[10];
            array[0] = null;
            array[1] = -1;
            array[2] = Int32.MaxValue;
            array[3] = Int32.MinValue;
            array[4] = 123456;
            array[5] = "Hi there";
            array[6] = 0;
            array[7] = UInt32.MaxValue;
            array[8] = (Int64)0x100000001;
            array[9] = (UInt64) 0x100000002;
            Object[] deserializedArray = SerializationRoundTrip(array);
            Assert.AreEqual(array.Length, deserializedArray.Length);

            for (int i = 0; i < array.Length; i++)
            {
                Object expected = array[i];
                Object deserializedValue = deserializedArray[i];
                if (deserializedValue != null && deserializedValue.GetType() == typeof(Int64))
                    expected = Convert.ChangeType(expected, typeof(Int64));
                Assert.AreEqual(expected, deserializedValue);
            }

            Console.WriteLine("Testing a List<Object>");
            List<Object> list = new List<Object>(array);
            List<Object> listCopy = SerializationRoundTrip(list);
            Assert.AreEqual(array.Length, deserializedArray.Length);
            for (int i = 0; i < array.Length; i++)
            {
                Object expected = array[i];
                Object deserializedValue = deserializedArray[i];
                if (deserializedValue != null && deserializedValue.GetType() == typeof(Int64))
                    expected = Convert.ChangeType(expected, typeof(Int64));
                Assert.AreEqual(expected, deserializedValue);
            }
        }
        
        [TestMethod]
        public void JsonObjectWrapperTest()
        {
            Console.WriteLine("JsonObjectWrapper tests");
            Exception exc;

            // Test the round-tripping of a complex object
            ComplexCustomTypeA ct1 = new ComplexCustomTypeA(11, 3.14);
            var ct1_json = SerializationHelper.Serialize(ct1);
            var wrapper = SerializationHelper.Deserialize(ct1_json);
            Assert.IsTrue(wrapper is JsonObjectWrapper, "Expected complex type to be deserialized as JsonObjectWrapper");
            Assert.AreEqual( ((JsonObjectWrapper)wrapper)["IntField"], 11L, "Error in JsonObjectWrapper string indexing");
            exc = null;
            try { object foo = ((JsonObjectWrapper)wrapper)[0]; }
            catch (Exception e) { exc = e; }
            Assert.IsNotNull(exc, "Expected int index into JObject-type JsonObjectWrapper to throw an exception");
            var ct1_json2 = SerializationHelper.Serialize(wrapper);
            var ct2 = SerializationHelper.DeserializeToType<ComplexCustomTypeA>(ct1_json2);
            Assert.IsTrue(ct2.Equals(ct1), "Round-tripped object was corrupted");

            // Test the round-tripping of an array
            int[] array1 = new int[] { 1, 3, 5, 7, 9 };
            var array1_json = SerializationHelper.Serialize(array1);
            var wrapper2 = SerializationHelper.Deserialize(array1_json);
            Assert.IsTrue(wrapper2 is JsonObjectWrapper, "Expected array to be deserialized as JsonObjectWrapper");
            Assert.AreEqual(((JsonObjectWrapper)wrapper2)[0], 1L, "Error in JsonObjectWrapper int indexing");
            var array1_json2 = SerializationHelper.Serialize(wrapper2);
            var array2 = SerializationHelper.DeserializeToType<int[]>(array1_json2);
            Assert.AreEqual(array1.Length, array2.Length, "Round-tripped array has different length than original");
            for (int i = 0; i < array1.Length; i++)
            {
                Assert.AreEqual(array1[i], array2[i], "Array comparison mismatch at element " + i);
            }

            // Test the round-tripping of a list
            List<string> list1 = new List<string>(new string[] {"foo", "bar", "spam"});
            var list1_json = SerializationHelper.Serialize(list1);
            var wrapper3 = SerializationHelper.Deserialize(list1_json);
            Assert.IsTrue(wrapper3 is JsonObjectWrapper, "Expected list to be deserialized as JsonObjectWrapper");
            var list1_json2 = SerializationHelper.Serialize(wrapper3);
            var list2 = SerializationHelper.DeserializeToType<List<string>>(list1_json2);
            Assert.AreEqual(list1.Count, list2.Count, "Round-tripped list has different count than original");
            for (int i = 0; i < list1.Count; i++)
            {
                Assert.AreEqual(list1[i], list2[i], "List comparison mismatch at element " + i);
            }
            
        }

        [TestMethod]
        public void SerializationHelperTests()
        {
            Console.WriteLine("SerializationHelper tests");
            byte[] messageBuf = new byte[1024];

            // Test ActorEvent round-trip
            ActorEvent ae = new ActorEvent("someType", Tuple.Create("Hi There", 42), 13, "Boo!");
            MemoryStream ms = new MemoryStream();
            SerializationHelper.SerializeActorEvent(ae, ms);
            ms.Position = 0;
            object obj = SerializationHelper.DeserializeActorFeedbackAsync(ms, messageBuf).Result;
            Assert.IsTrue(obj is ActorEvent, "Expected deserialized feedback to be ActorEvent");
            ActorEvent ae2 = obj as ActorEvent;
            VerifyEventsSame<Tuple<string, int>>(ae, ae2);

            // Test ActorServiceRequest round trip
            ActorServiceRequest req = new ActorServiceRequest("someMethod", "haha", 42, (byte)0, "This needs to work", 42.424242);
            ms = new MemoryStream();
            SerializationHelper.SerializeActorServiceRequest(req, ms);
            ms.Position = 0;
            var req2 = SerializationHelper.DeserializeActorServiceRequestAsync(ms, messageBuf).Result;
            VerifyRequestsSame(req, req2);

            // Test ActorServiceResult round trip (success)
            ActorServiceResult response1 = ActorServiceResult.FromResult(1234, "My Result!");
            ms = new MemoryStream();
            SerializationHelper.SerializeActorServiceResult(response1, ms);
            ms.Position = 0;
            obj = SerializationHelper.DeserializeActorFeedbackAsync(ms, messageBuf).Result;
            Assert.IsTrue(obj is ActorServiceResult, "Expected deserialized feedback to be ActorServiceResult");
            var response2 = obj as ActorServiceResult;
            VerifyResultsSame<string>(response1, response2);

            // Test ActorServiceResult round trip (failure)
            response1 = ActorServiceResult.FromException(4321, new ArgumentException("Stop Arguing!"));
            ms = new MemoryStream();
            SerializationHelper.SerializeActorServiceResult(response1, ms);
            ms.Position = 0;
            obj = SerializationHelper.DeserializeActorFeedbackAsync(ms, messageBuf).Result;
            Assert.IsTrue(obj is ActorServiceResult, "Expected deserialized feedback to be ActorServiceResult");
            response2 = obj as ActorServiceResult;
            VerifyResultsSame<string>(response1, response2);
           
        }

        [TestMethod]
        public void NetworkHelperTests()
        {
            Console.WriteLine("NetworkHelper tests");

            // Test bytes->int
            byte[] bytes = new byte[] { 0x12, 0x34, 0x00, 0x00 };
            int i = NetworkHelpers.BytesToInt(bytes);
            Assert.AreEqual(i, 0x3412, "Unexpected BytesToInt result");
            bytes = new byte[] { 0x78, 0x56, 0x34, 0x12 };
            i = NetworkHelpers.BytesToInt(bytes);
            Assert.AreEqual(i, 0x12345678, "Unexpected BytesToInt result");
            bytes = new byte[] { 0xFF, 0xFF, 0x01, 0x00 };
            i = NetworkHelpers.BytesToInt(bytes);
            Assert.AreEqual(i, 0x1FFFF, "Unexpected BytesToInt result");
            bytes = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
            i = NetworkHelpers.BytesToInt(bytes);
            Assert.AreEqual(i, -1, "Unexpected BytesToInt result");

            // Test int -> bytes
            i = -1;
            bytes = NetworkHelpers.IntToBytes(i);
            Assert.AreEqual(bytes.Length, 4, "Expected IntToBytes to return 4-byte array");
            for (int j = 0; j < bytes.Length; j++) Assert.AreEqual(bytes[j], 0xFF, "Unexpected IntToBytes result");

            i = 0x1FFFFFFF;
            bytes = NetworkHelpers.IntToBytes(i);
            Assert.AreEqual(bytes.Length, 4, "Expected IntToBytes to return 4-byte array");
            for (int j = 0; j < bytes.Length; j++) Assert.AreEqual(bytes[j], j == 3 ? 0x1F : 0xFF, "Unexpected IntToBytes result");

            i = 0x1FFFF;
            bytes = NetworkHelpers.IntToBytes(i);
            Assert.AreEqual(bytes.Length, 4, "Expected IntToBytes to return 4-byte array");
            for (int j = 0; j < bytes.Length; j++) Assert.AreEqual(bytes[j], j == 3 ? 0x00 : j == 2 ? 0x01 : 0xFF, "Unexpected IntToBytes result");

        }

#if false
        // Removing BigInteger support for now, so unlinking this test
        //[TestMethod]
        public void BigIntegerSerializationTest()
        {
            Console.WriteLine("Basic serialization test");

            // Unfortunately, JavaScript seems to be be missing a Decimal type.
            // So, deserializing a Decimal through JSON.NET produces a double.
            /*
            Console.WriteLine("Testing Decimal");
            Decimal d = Decimal.MaxValue;
            Decimal deserializedDecimal = SerializationRoundTrip(d);
            Assert.AreEqual(d, deserializedDecimal);
            */

            Console.WriteLine("Testing a BigInteger");
            BigInteger five = new BigInteger(5);
            BigInteger deserialized = SerializationRoundTrip(five);
            Console.WriteLine("Serialized {0}, and deserialized {1}", five, deserialized);
            Assert.AreEqual(five, deserialized);

            Console.WriteLine("Testing an Object[] containing some BigIntegers");
            Object[] array = new Object[4];
            array[0] = null;
            array[1] = BigInteger.MinusOne;
            array[2] = new BigInteger(Int64.MaxValue);
            array[3] = "Hi there";
            Object[] deserializedArray = SerializationRoundTrip(array);
            Assert.AreEqual(array.Length, deserializedArray.Length);

            for (int i = 0; i < array.Length; i++)
                Assert.AreEqual(array[i], deserializedArray[i]);

            Console.WriteLine("Testing a List<Object>");
            List<Object> list = new List<Object>(array);
            List<Object> listCopy = SerializationRoundTrip(list);
            Assert.AreEqual(array.Length, deserializedArray.Length);
            for (int i = 0; i < array.Length; i++)
                Assert.AreEqual(array[i], deserializedArray[i]);
        }
#endif

        private static T SerializationRoundTrip<T>(T value)
        {
            String serializedData = SerializationHelper.Serialize(value);
            T deserialized = SerializationHelper.DeserializeToType<T>(serializedData);
            return deserialized;
        }

        //
        // Comparison utilities
        //

        private static void VerifyRequestsSame(ActorServiceRequest req1, ActorServiceRequest req2)
        {
            Assert.IsTrue(req1.MethodName.Equals(req2.MethodName), "Method name mismatch");
            Assert.IsTrue(req1.UniqueClientId.Equals(req2.UniqueClientId), "UniqueClientId mismatch");
            Assert.IsTrue(req1.ClientSequenceNumber == req2.ClientSequenceNumber, "ClientSequenceNumber mismatch");
            Assert.IsTrue((req1.MethodArgs == null) == (req2.MethodArgs == null), "MethodArgs null-check mismatch");
            if (req1.MethodArgs != null)
            {
                Assert.IsTrue(req1.MethodArgs.Length == req2.MethodArgs.Length, "MethodArgs length mismatch");
                for (int i = 0; i < req1.MethodArgs.Length; i++)
                {
                    Assert.AreEqual(req1.MethodArgs[i], req2.MethodArgs[i]);
                }
            }
        }

        private static void VerifyEventsSame<T>(ActorEvent e1, ActorEvent e2)
        {
            Assert.AreEqual(e1.EventSource == null, e2.EventSource == null, "EventSource null-check mismatch");
            if (e1.EventSource != null)
            {
                Assert.IsTrue(e1.EventSource.Equals(e2.EventSource), "EventSource mismatch");
            }

            Assert.IsTrue(e1.EventType.Equals(e2.EventType), "EventType mismatch");

            Assert.AreEqual(e1.EventPayload == null, e2.EventPayload == null, "EventPayload null-check mismatch");
            if (e1.EventPayload != null)
            {
                Assert.IsTrue(e1.EventPayload.Equals(e2.EventPayload), "EventPayload mismatch");
                var p1 = e1.GetTypedPayload<T>();
                var p2 = e2.GetTypedPayload<T>();
                Assert.AreEqual(p1, p2, "Typed EventPayload mismatch");
            }

            Assert.AreEqual(e1.EventPayloadQualifiers == null, e2.EventPayloadQualifiers == null, "Qualifiers null-check mismatch");
            if (e1.EventPayloadQualifiers != null)
            {
                Assert.AreEqual(e1.EventPayloadQualifiers.Length, e2.EventPayloadQualifiers.Length, "Qualifiers length mismatch");
                for (int i = 0; i < e1.EventPayloadQualifiers.Length; i++)
                {
                    var q1 = e1.EventPayloadQualifiers[i];
                    var q2 = e2.EventPayloadQualifiers[i];
                    Assert.AreEqual(q1, q2, "Qualifier mismatch at index " + i);
                }
            }
        }

        private static void VerifyResultsSame<T>(ActorServiceResult r1, ActorServiceResult r2)
        {
            Assert.AreEqual(r1.Status, r2.Status, "Status mismatch");

            Assert.AreEqual(r1.Result == null, r2.Result == null, "Result null-check mismatch");
            if (r1.Result != null)
            {
                Assert.AreEqual(r1.Result, r2.Result, "Untyped Result mismatch");
                var res1 = r1.GetTypedResult<T>();
                var res2 = r2.GetTypedResult<T>();
                Assert.AreEqual(res1, res2, "Typed Result mismatch");
            }

            Assert.AreEqual(r1.IsIntResult, r2.IsIntResult, "IsIntResult mismatch");

            Assert.AreEqual(r1.ClientSequenceNumber, r2.ClientSequenceNumber, "ClientSequenceNumber mismatch");

            Assert.AreEqual(r1.ExceptionType == null, r2.ExceptionType == null, "ExceptionType null-check mismatch");
            if (r1.ExceptionType != null)
            {
                Assert.AreEqual(r1.ExceptionType, r2.ExceptionType, "ExceptionType mismatch");
            }

            Assert.AreEqual(r1.ExceptionMessage == null, r2.ExceptionMessage == null, "ExceptionMessage type-check mismatch");
            if (r1.ExceptionMessage != null)
            {
                Assert.AreEqual(r1.ExceptionMessage, r2.ExceptionMessage, "ExceptionMessage mismatch");
            }

            Assert.AreEqual(r1.ExceptionStackTrace == null, r2.ExceptionStackTrace == null, "ExceptionStackTrace type-check mismatch");
            if (r1.ExceptionStackTrace != null)
            {
                Assert.AreEqual(r1.ExceptionStackTrace, r2.ExceptionStackTrace, "ExceptionStackTrace mismatch");
            }
        }


    }

    internal class ComplexCustomTypeA
    {
        public int IntField;
        public string StringField;
        public double DoubleField;
        public int[] IntArrayField;
        public DateTime DateField;
        public ComplexCustomTypeB CustomTypeField;

        public ComplexCustomTypeA(int intArg, double doubleArg)
        {
            IntField = intArg;
            StringField = intArg.ToString();
            DoubleField = doubleArg;
            IntArrayField = new int[] { intArg, intArg + 1, intArg + 2 };
            CustomTypeField = new ComplexCustomTypeB() { DoubleField = doubleArg, LongField = 0x100000000, ByteField=0x19, DTOField = new DateTimeOffset(DateTime.Now) };
            DateField = DateTime.Now;
        }

        public override bool Equals(object obj)
        {
            var other = obj as ComplexCustomTypeA;
            if (other == null) return false;
            if (IntField != other.IntField) return false;
            if (!StringField.Equals(other.StringField)) return false;
            if (DoubleField != other.DoubleField) return false;
            if ((DateField == null) != (other.DateField == null)) return false;
            if (DateField != null)
            {
                if (!DateField.Equals(other.DateField)) return false;
            }
            if ((IntArrayField == null) != (other.IntArrayField == null)) return false;
            if (IntArrayField != null)
            {
                if (IntArrayField.Length != other.IntArrayField.Length) return false;
                for (int i = 0; i < IntArrayField.Length; i++)
                {
                    if (IntArrayField[i] != other.IntArrayField[i]) return false;
                }
            }
            if ((CustomTypeField == null) != (other.CustomTypeField == null)) return false;
            if (CustomTypeField != null)
            {
                if (!CustomTypeField.Equals(other.CustomTypeField)) return false;
            }

            return true;
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

    internal class ComplexCustomTypeB
    {
        public double DoubleField;
        public Guid GuidField = Guid.NewGuid();
        public long LongField;
        public byte ByteField;
        public DateTimeOffset DTOField;

        public override bool Equals(object obj)
        {
            ComplexCustomTypeB other = obj as ComplexCustomTypeB;
            if (other == null) return false;
            if(DoubleField != other.DoubleField) return false;
            if (!GuidField.Equals(other.GuidField)) return false;
            if (LongField != other.LongField) return false;
            if (ByteField != other.ByteField) return false;
            if ((DTOField == null) != (other.DTOField == null)) return false;
            if (DTOField != null)
            {
                if (!DTOField.Equals(other.DTOField)) return false;
            }

            return true;
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }
}
